package media.plugins.timeline;

import javax.media.*;

import com.sun.media.BasicClock;

// This Controller uses two custom classes:
//    The base class is EventPostingBase.  It has three methods:
//      public void addControllerListener (ControllerListener 
//         observer);
//      public void removeControllerListener (ControllerListener
//         observer);
//      protected void postEvent (ControllerEvent event);
//

//   This Controller posts TimeLineEvents.  TimeLineEvent has 
//   two methods:
//      public TimeLineEvent (Controller who, int
//         segmentEntered);
//      public final int getSegment ();

public class TimeLineController extends EventPostingBase 
implements Controller, Runnable 
{
	Clock ourClock;

	// This simple controller really only has two states: 
	// Prefetched and Started.

	int ourState;
	long timeLine[];
	int currentSegment = -1;
	long duration;
	Thread myThread;

	// Create a TimeLineController giving it a sorted time line.  
	// The TimeLineController will post events indicating when 
	// it has passed to different parts of the time line.

	public TimeLineController (long timeLine[]) 
	{
		this.timeLine = timeLine;
		ourClock = new BasicClock ();

		duration = timeLine[timeLine.length-1];
		myThread = null;
		// We always start off ready to go!
		ourState = Controller.Prefetched;
	}

	// Binary search for which segment we are now in.  Segment 
	// 0 is considered to start at 0 and end at timeLine[0].  
	// Segment timeLine.length is considered to start at 
	// timeLine[timeLine.length-1] and end at infinity.  At the 
	// points of 0 and timeLine[timeLine.length-1] the
	// Controller will stop (and post an EndOfMedia event).    

	int computeSegment (long time) 
	{
		int max = timeLine.length;
		int min = 0;

		for (;;) 
		{
			if (min == max) return min;
			int current = min + ((max - min) >> 1);

			if (time < timeLine[current]) 
			{
				max = current;
			} 

			else 
			{
				min = current + 1;
			}
		}
	}
	// These are all simple...

	public float setRate (float factor) 
	{
		// We don't support a rate of 0.0.  Not worth the extra math 
		// to handle something the user should do with the stop() 
		// method!

		if (factor == 0.0f) 
		{
			factor = 1.0f;
		}

		float newRate = ourClock.setRate (factor);

		postEvent (new RateChangeEvent (this, newRate));
		return newRate;
	}

	public void setTimeBase (TimeBase master) 
	throws IncompatibleTimeBaseException 
	{
		ourClock.setTimeBase (master);
	}

	public Time getStopTime () 
	{
		return ourClock.getStopTime ();
	}

	public Time getSyncTime () 
	{
		return ourClock.getSyncTime ();
	}

	public Time mapToTimeBase (Time t) throws ClockStoppedException 
	{
		return ourClock.mapToTimeBase (t);
	}

	public Time getMediaTime () 
	{
		return ourClock.getMediaTime ();
	}

	public TimeBase getTimeBase () 
	{
		return ourClock.getTimeBase ();
	}
	public float getRate () 
	{
		return ourClock.getRate ();
	}

	// From Controller

	public int getState () 
	{
		return ourState;
	}

	public int getTargetState () 
	{
		return ourState;
	}

	public void realize () 
	{
		postEvent (new RealizeCompleteEvent (this, ourState, 
				ourState, ourState));
	}

	public void prefetch () 
	{
		postEvent (new PrefetchCompleteEvent (this, ourState, 
				ourState, ourState));
	}

	public void deallocate () 
	{
		postEvent (new DeallocateEvent (this, ourState, 
				ourState, ourState, ourClock.getMediaTime ()));
	}

	public Time getStartLatency () 
	{
		// We can start immediately, of course!

		return new Time(0);
	}

	public Control[] getControls () 
	{
		return new Control[0];
	}
	public Time getDuration () 
	{
		return new Time(duration);
	}

	// This one takes a little work as we need to compute if we 
	// changed segments.

	public void setMediaTime (Time now) 
	{
		ourClock.setMediaTime (now);
		postEvent (new MediaTimeSetEvent (this, now));

		checkSegmentChange (now.getNanoseconds());
	}

	// We now need to spin a thread to compute/observe the 
	// passage of time.

	public synchronized void syncStart (Time tbTime) 
	{
		long startTime = ourClock.getMediaTime().getNanoseconds();

		// We may actually have to stop immediately with an 
		// EndOfMediaEvent. We compute that now.  If we are already   
		// past end of media, then we
		// first post the StartEvent then we post a EndOfMediaEvent

		boolean endOfMedia;
		float rate = ourClock.getRate ();

		if ((startTime > duration && rate >= 0.0f) || 
				(startTime < 0 && rate <= 0.0f)) 
		{
			endOfMedia = true;
		} 

		else 
		{
			endOfMedia = false;
		}

		// We face the same possible problem with being past the stop 
		// time.  If so, we stop immediately.

		boolean pastStopTime;
		long stopTime = ourClock.getStopTime().getNanoseconds();
		if ((stopTime != Long.MAX_VALUE) && 
				((startTime >= stopTime && rate >= 0.0f) ||
						(startTime <= stopTime && rate <= 0.0f))) 
		{
			pastStopTime = true;
		} 

		else 
		{
			pastStopTime = false;
		}

		if (!endOfMedia && !pastStopTime) 
		{

			ourClock.syncStart (tbTime);
			ourState = Controller.Started;
		}

		postEvent (new StartEvent (this, Controller.Prefetched, 
				Controller.Started, Controller.Started, 
				new Time(startTime), tbTime));

		if (endOfMedia) 
		{
			postEvent (new EndOfMediaEvent (this,
					Controller.Started,
					Controller.Prefetched, Controller.Prefetched,
					new Time(startTime)));
		} 

		else if (pastStopTime) 
		{
			postEvent (new StopAtTimeEvent (this, Controller.Started,
					Controller.Prefetched, Controller.Prefetched,
					new Time(startTime)));
		} 

		else 
		{
			myThread = new Thread (this, "TimeLineController");

			// Set thread to appopriate priority...
			myThread.start ();
		}
	}



	// Nothing really special here except that we need to notify 
	// the thread that we may have.

	public synchronized void setStopTime (Time stopTime) 
	{
		ourClock.setStopTime (stopTime);
		postEvent (new StopTimeChangeEvent (this, stopTime));
		notifyAll ();
	}

	// This one is also pretty easy.  We stop and tell the running
	// thread to exit.

	public synchronized void stop () 
	{
		int previousState = ourState;
		ourClock.stop ();
		ourState = Controller.Prefetched;
		postEvent (new StopByRequestEvent (this, previousState,
				Controller.Prefetched, Controller.Prefetched,
				ourClock.getMediaTime ()));
		notifyAll ();

		// Wait for thread to shut down.

		while (myThread != null) 
		{
			try 
			{
				wait ();
			} 
			catch (InterruptedException e) 
			{
				// NOT REACHED
			}
		}
	}

	protected void checkSegmentChange (long timeNow) 
	{
		int segment = computeSegment (timeNow);
		if (segment != currentSegment) 
		{
			currentSegment = segment;
			postEvent (new TimeLineEvent (this, currentSegment));
		}
	}

	// Most of the real work goes here.  We have to decide when 
	// to post events like EndOfMediaEvent and StopAtTimeEvent 
	// and TimeLineEvent.

	public synchronized void run () 
	{
		long timeToNextSegment = 0;
		long mediaTimeToWait = 0;

		float ourRate = 1.0f;


		for (;;) 
		{
			// First, have we changed segments?  If so, post an event!

			long timeNow = ourClock.getMediaTime ().getNanoseconds ();
			checkSegmentChange (timeNow);

			// Second, have we already been stopped?  If so, stop 
			// the thread.

			if (ourState == Controller.Prefetched) 
			{
				myThread = null;

				// If someone is waiting for the thread to die, let them 
				// know.

				notifyAll ();
				break;
			}

			// Current rate.  Our setRate() method prevents the value 
			// 0 so we don't check for that here.

			ourRate = ourClock.getRate ();

			// How long in clock time do we need to wait before doing
			// something?

			long endOfMediaTime;

			// Next, are we past end of media?

			if (ourRate > 0.0f) 
			{
				mediaTimeToWait = duration - timeNow;
				endOfMediaTime = duration;
			} 
			else 
			{
				mediaTimeToWait = timeNow;
				endOfMediaTime = 0;
			}

			// If we are at (or past) time to stop due to EndOfMedia, 
			// we do that now!

			if (mediaTimeToWait <= 0) 
			{
				ourClock.stop ();
				ourClock.setMediaTime (new Time(endOfMediaTime));
				ourState = Controller.Prefetched;
				postEvent (new EndOfMediaEvent (this, Controller.Started,
						Controller.Prefetched, Controller.Prefetched,
						new Time(endOfMediaTime)));
				continue;
			}

			// How long until we hit our stop time?

			long stopTime = ourClock.getStopTime ().getNanoseconds();
			if (stopTime != Long.MAX_VALUE) 
			{
				long timeToStop;
				if (ourRate > 0.0f) 
				{
					timeToStop = stopTime - timeNow;
				} 
				else 
				{
					timeToStop = timeNow - stopTime;
				}

				// If we are at (or past) time to stop due to the stop 
				// time, we stop now!
				if (timeToStop <= 0) 
				{
					ourClock.stop ();
					ourClock.setMediaTime (new Time(stopTime));
					ourState = Controller.Prefetched;
					postEvent (new StopAtTimeEvent (this,
							Controller.Started,
							Controller.Prefetched, Controller.Prefetched,
							new Time(stopTime)));
					continue;
				} 
				else if (timeToStop < mediaTimeToWait) 
				{
					mediaTimeToWait = timeToStop;
				}
			}

			// How long until we pass into the next time line segment?

			if (ourRate > 0.0f) 
			{
				timeToNextSegment = timeLine[currentSegment] - timeNow;
			} 

			else if (currentSegment == 0) 
			{
				timeToNextSegment = timeNow;
			} 

			else 
			{
				timeToNextSegment = timeNow - timeLine[currentSegment-1];
			}
		}

		if (timeToNextSegment < mediaTimeToWait) 
		{
			mediaTimeToWait = timeToNextSegment;
		}

		// Do the ugly math to compute what value to pass to 
		// wait():

		long waitTime;
		if (ourRate > 0) 
		{
			waitTime = (long) ((float) mediaTimeToWait / ourRate)  /
			1000000;
		}
		else 
		{
			waitTime = (long) ((float) mediaTimeToWait / -ourRate) /
			1000000;
		}
		// Add one because we just rounded down and we don't 
		// really want to waste CPU being woken up early.

		waitTime++;

		if (waitTime > 0) 
		{
			// Bug in some systems deals poorly with really large 
			// numbers. We will cap our wait() to 1000 seconds 
			// which point we will probably decide to wait again.

			if (waitTime > 1000000) waitTime = 1000000;
			try 
			{
				wait (waitTime);
			} 
			catch (InterruptedException e) 
			{
				// NOT REACHED
			}
		}
	}

	public void close()
	{

	}

	public Control getControl(String type)
	{
		return null;
	}

	public long getMediaNanoseconds()
	{
		return 0;
	}

	@Override
	public void addControllerListener(ControllerListener arg0) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void removeControllerListener(ControllerListener arg0) {
		// TODO Auto-generated method stub
		
	} 
}

